// dOOdad - Object-oriented programming framework for Javascript
// File: Loader.js - Optional Loader
// Project home: https://sourceforge.net/projects/doodad-js/
// Author: Claude Petit, Quebec city
// Copyright @ 2015

if (!window.Doodad) {
	Doodad = {
		parent: window,
	};
};

if (!Doodad.Loader) {
	Doodad.Loader = {
		parent: Doodad,
		
		defaultAsync: true,
	};
};

Doodad.Loader.init = function() {
	"use strict";
	
	var loader = this;

	if (loader.initialized) {
		return;
	};

	var doodad = loader.parent;
	
	var tools = doodad.Tools;
	if (!tools || !tools.init) {
		throw new Error("'Tools.js' is not loaded.");
	};
	if (!tools.initialized) {
		tools.init();
	};


	function __initScripts__(before, scriptsBefore, scriptsAfter) {
		var ok = false,
			scriptsDefs = (before ? scriptsBefore : scriptsAfter),
			scriptsDef, scripts, depsDefs, depsDef, exprsDef, inits, exprs, expr, val, result, beforeScriptsDef, beforeDepsDefs, beforeDepsDef, beforeExprsDef, beforeExprs, thisObj,
			i = scriptsDefs.length,
			j,
			k,
			removed;
		
		do {
			removed = false;

			while (i && scriptsDefs.length) {
				scriptsDef = scriptsDefs[0] || {};
				depsDefs = scriptsDef.dependencies || [];
				
				j = 0;
				while (j < depsDefs.length) {
					depsDef = depsDefs[j] || {};
					exprsDef = depsDef.conditions || {};
					scripts = depsDef.scripts || [];
					inits = depsDef.initializers || [];
					
					// Evaluate "exclude" conditions...
					exprs = exprsDef.exclude || [];  // "or" conditions
					result = false;
					k = 0;
					while (k < exprs.length) {
						val = false;
						try {
							expr = exprs[k];
							if (tools.isObject(expr)) {
								thisObj = eval(expr.thisObj || undefined);
								val = eval(expr._function).call(thisObj);
							} else {
								val = eval(expr);
							};
						} catch(ex) {
							loader.lastEx = ex;
						};
						if (val) {
							exprs.length = 0;
							result = true;
						} else {
							k++;
						};
					};

					exprs = exprsDef.include || []; // "and" conditions
					if (result) {
						// When "exclude" returns "true", ignore this dependency.
						exprs.length = 0;
						scripts.length = 0;
						inits.length = 0;
					} else {
						// When "exclude" returns "false", evaluate "include" condition...
						result = true;
						k = 0;
						while (k < exprs.length) {
							expr = exprs[k];
							
							if (!before && scriptsBefore) {
								for (var l = 0; l < scriptsBefore.length; l++) {
									beforeScriptsDef = scriptsBefore[l];
									beforeDepsDefs = beforeScriptsDef.dependencies || [];
									for (var m = 0; m < beforeDepsDefs.length; m++) {
										beforeDepsDef = beforeDepsDefs[m];
										beforeExprsDef = beforeDepsDef.conditions || {};
										beforeExprs = beforeExprsDef.include;
										for (var n = 0; n < beforeExprs.length; n++) {
											if (beforeExprs[n] === expr) {
												result = false;
												break;
											};
										};
										if (!result) {
											break;
										};
									};
									if (!result) {
										break;
									};
								};
							};

							if (result) {
								val = false;
								try {
									if (tools.isObject(expr)) {
										thisObj = eval(expr.thisObj || undefined);
										val = eval(expr._function).call(thisObj);
									} else {
										val = eval(expr);
									};
								} catch(ex) {
									loader.lastEx = ex;
								};
								if (!val) {
									result = false;
								};
							};
							
							if (result) {
								exprs.splice(k, 1);
							} else {
								k++;
							};
						};
					};
					
					// If there are remaining "include" conditions, stop to process this dependency
					if (exprs.length) {
						break;
					};

					if ((scripts.deletedItems || 0) >= scripts.length) {
						// All scripts have been deleted
						scripts.length = 0;
					};
					if (!scripts.length) {
						// When every "include" conditions are "true", and every scripts are loaded, execute initializers...
						while (inits.length) {
							expr = inits[0];
							if (doodad.ASSERT) {
								// Debug mode
								if (tools.isObject(expr)) {
									thisObj = eval(expr.thisObj || undefined);
									eval(expr._function).call(thisObj);
								} else {
									eval(expr);
								};
							} else {
								try {
									if (tools.isObject(expr)) {
										thisObj = eval(expr.thisObj || undefined);
										eval(expr._function).call(thisObj);
									} else {
										eval(expr);
									};
								} catch(ex) {
									loader.lastEx = ex;
									break;
								};
							};
							inits.splice(0, 1);
						};
					};

					// When every "initializers" are executed, and every scripts loaded, remove dependency...
					if (scripts.length || inits.length) {
						j++;
					} else {
						depsDefs.splice(j, 1);
					};
				};
				
				scriptsDefs.splice(0, 1);
				if (depsDefs.length) {
					// Missing dependencies, move last
					scriptsDefs.splice(i + 1, 0, scriptsDef);
					i--;
				} else {
					// When every dependencies are meet, remove entry...
					ok = true;
					removed = true;
				};
			};
		} while (removed);

		return ok;
	};
	
	function __loadMissings__(before, scriptsBefore, scriptsAfter, reload, async) {
		var ok = false,
			loading,
			scriptsDefs = (before ? scriptsBefore : scriptsAfter),
			scriptsDef, depsDefs, depsDef, exprsDef, exprs, scripts, inits, scriptDef, scriptType, url,
			i = 0,
			j,
			k,
			scriptLoaders = [];
			
		function makeHandler(scriptLoader, scripts, inits, scriptsDefs, scriptDef, depsDefs, i, j, k) {
			return function loadScriptHandler(ev) {
				if (!this.failed || scriptDef.optional) {
					delete scripts[k];
					scripts.deletedItems++;
				};
				if (scripts.deletedItems >= scripts.length) {
					// All scripts have been deleted
					scripts.length = 0;
				};
				if (!scripts.length && !inits.length) {
					depsDefs.splice(j, 1);
				};
				if (!depsDefs.length) {
					scriptsDefs.splice(i, 1);
				};
				loader.remaining--;
				if (loader.remaining === 0) {
					loader.loadScripts(scriptsBefore, scriptsAfter, reload, async);
				};
			};
		};
			
		while (i < scriptsDefs.length) {
			scriptsDef = scriptsDefs[i] || {};
			depsDefs = scriptsDef.dependencies || [];
			
			loading = false;
			j = 0;
			while (j < depsDefs.length) {
				depsDef = depsDefs[j] || {};
				exprsDef = depsDef.conditions || {};
				exprs = exprsDef.include || [];
				scripts = depsDef.scripts || [];
				inits = depsDef.initializers || [];

				if (!tools.hasKey(scripts, 'deletedItems')) {
					scripts.deletedItems = 0;
				};

				// If ready to load scripts...
				if (!exprs.length) {
					// Add dependent scripts...
					exprs = exprsDef.exclude || [];
					for (k = 0; k < exprs.length; k++) {
						var expr = exprs[k];
						
						for (var l = 0; l < scriptsDefs.length; l++) {
							var scriptsDef2 = scriptsDefs[l],
								depsDefs2 = scriptsDef2.dependencies || [];
								
							for (var m = 0; m < depsDefs2.length; m++) {
								var depsDef2 = depsDefs2[m] || {},
									exprsDef2 = depsDef2.conditions || {},
									exprs2 = exprsDef2.include || [],
									scripts2 = depsDef2.scripts || [];
									
								if (scripts2.length) {
									for (var n = 0; n < exprs2.length; n++) {
										if (exprs2[n] === expr) {
											tools.append(scripts, scripts2);
											scripts2.length = 0;
											break;
										};
									};
								};
							};
						};
					};

					// Load scripts
					for (k = 0; k < scripts.length; k++) {
						if (k in scripts) {
							scriptDef = scripts[k];
							scriptType = scriptDef.fileType || 'js';
							
							url = scriptDef.baseUrl || null;
							if (tools.isFunction(url)) {
								url = url() || '';
							};
							if (tools.isString(url)) {
								url = tools.parseUrl(url);
							};
							if (!(url instanceof tools.url)) {
								url = new tools.url();
							};
							var file = scriptDef.fileName;
							if (tools.isFunction(file)) {
								file = file() || '';
							};
							url.file = file;
							url = url.toString();
							
							var scriptLoader = null;
							if (scriptType === 'js') {
								scriptLoader = tools.getJsScriptFileLoader(/*url*/url, /*async*/async, /*timeout*/scriptDef.timeout, /*reload*/reload);
							} else if (scriptType === 'css') {
								async = false;
								scriptLoader = tools.getCssScriptFileLoader(/*url*/url, /*async*/async, /*media*/scriptDef.media, /*timeout*/scriptDef.timeout, /*reload*/reload);
							};
							
							if (scriptLoader) {
								var handler = makeHandler(scriptLoader, scripts, inits, scriptsDefs, scriptDef, depsDefs, i, j, k);
								
								if (scriptLoader.launched) {
									// Script has already been asked
									if (scriptLoader.ready) {
										if (!scriptLoader.failed || scriptDef.optional) {
											// Script is already loaded
											delete scripts[k];
											scripts.deletedItems++;
											loading = true;
										};
									};
								} else {
									// Load new script
									loader.remaining++;
									scriptLoader.addEventListener('load', handler);
									scriptLoader.addEventListener('error', handler);
									scriptLoaders.push(scriptLoader);
									loading = true;
								};
							};
						};
					};
				};
				
				if (scripts.deletedItems >= scripts.length) {
					// All scripts have been deleted
					scripts.length = 0;
				};
				if (!scripts.length && !inits.length) {
					depsDefs.splice(j, 1);
				} else {
					j++;
				};
			};
			
			if (!depsDefs.length) {
				scriptsDefs.splice(i, 1);
				ok = true;
			} else {
				if (loading) {
					// Move first
					scriptsDefs.splice(i, 1);
					scriptsDefs.unshift(scriptsDef);
					ok = true;
				};
				i++;
			};
		};
		
		// Start loaders
		if (scriptLoaders.length) {
			for (var i = 0; i < scriptLoaders.length; i++) {
				scriptLoaders[i].start();
			};
		};
		
		return ok;
	};

	function __dumpFailed__(scriptsDefs) {
		// TODO: Make a more "easy to understand" dump
		var scriptsDef,
			optionalDepsDefs,
			optionalScriptDefs,
			i = 0;
		for (; i < scriptsDefs.length; i++) {
			scriptsDef = scriptsDefs[i] || {};
			optionalDepsDefs = tools.filter(scriptsDef.dependencies || [], function(depsDef) {
				return depsDef.optional;
			});
			if (!optionalDepsDefs.length) {
				tools.log(tools.LogLevels.Error, scriptsDef);
			};
		};
	};
	
	function __areOptional__(scriptsDefs) {
		return !tools.some(scriptsDefs, function(scriptsDef) {
			var depsDefs = scriptsDef.dependencies || [];
			return tools.some(depsDefs, function(depsDef) {
				var exprsDef = depsDef.conditions || {};
				return !exprsDef.optional;
			});
		});
	};
	
	function __loadScripts__(/*optional*/scriptsBefore, /*optional*/scriptsAfter, /*optional*/reload, /*optional*/async) {
		if (tools.isNothing(doodad.scriptsBefore)) {
			doodad.scriptsBefore = [];
		};
		if (tools.isNothing(doodad.scriptsAfter)) {
			doodad.scriptsAfter = [];
		};
		if (doodad.ASSERT) {
			doodad.ASSERT(tools.isArray(doodad.scriptsBefore), 'Invalid "scripts before" array.');
			doodad.ASSERT(tools.isArray(doodad.scriptsAfter), 'Invalid "scripts after" array.');
		};
		if (tools.isNothing(scriptsBefore) && tools.isNothing(scriptsAfter)) {
			scriptsBefore = doodad.scriptsBefore;
			scriptsAfter = doodad.scriptsAfter;
		} else {
			if (tools.isNothing(scriptsBefore)) {
				scriptsBefore = [];
			} else if (tools.isNothing(scriptsAfter)) {
				scriptsAfter = [];
			};
			if (doodad.ASSERT) {
				doodad.ASSERT(tools.isArray(scriptsBefore), 'Invalid "scripts before" array.');
				doodad.ASSERT(tools.isArray(scriptsAfter), 'Invalid "scripts after" array.');
			};
			if (scriptsBefore !== doodad.scriptsBefore) {
				tools.prepend(scriptsBefore, doodad.scriptsBefore);
				doodad.scriptsBefore.length = 0;
			};
			if (scriptsAfter !== doodad.scriptsAfter) {
				tools.append(scriptsAfter, doodad.scriptsAfter);
				doodad.scriptsAfter.length = 0;
			};
		};
		reload = !!reload;
		if (tools.isNothing(async)) {
			async = !!loader.defaultAsync;
		};

		var done = true;
		loader.remaining = 0;
		loader.lastEx = null;
		loader.repeatingLoadScripts = false;
		
		if (scriptsBefore.length || scriptsAfter.length) {
			loader.dispatchEvent(new tools.CustomEvent('loading'));
			
			while (scriptsBefore.length || scriptsAfter.length) {
				var ok = __initScripts__(true, scriptsBefore, scriptsAfter);
				ok = __initScripts__(false, scriptsBefore, scriptsAfter) || ok;
				
				if (!ok) {
					ok = __loadMissings__(true, scriptsBefore, scriptsAfter, reload, async);
					ok = __loadMissings__(false, scriptsBefore, scriptsAfter, reload, async) || ok;
					
					if (ok) {
						loader.dispatchEvent(new tools.CustomEvent('waiting'));
						
					} else {
						ok = __areOptional__(scriptsBefore) && __areOptional__(scriptsAfter);
						
						if (!ok) {
							done = false;
							if (doodad.ASSERT) {
								tools.log(tools.LogLevels.Error, "The following scripts failed to load or to initialize :");
								__dumpFailed__(scriptsBefore);
								__dumpFailed__(scriptsAfter);
								debugger;
							};
						};
						
						scriptsBefore.length = 0;
						scriptsAfter.length = 0;
					};
					
					break;
				};
			};
			
			if (done) {
				loader.dispatchEvent(new tools.CustomEvent('success'));
			} else {
				loader.dispatchEvent(new tools.CustomEvent('error'));
			};
		};
		
		if (done) {
			loader.startup = false;
		};
		
		return done;
	};


	// Internal flags
	loader.remaining = 0;
	loader.lastEx = null;
	loader.repeatingLoadScripts = false;
	loader.startup = false;
	
	loader.loadScripts = function loadScripts(/*optional*/scriptsBefore, /*optional*/scriptsAfter, /*optional*/reload, /*optional*/async) {
		if (loader.startup || doodad.ASSERT) {
			// Debug mode
			return __loadScripts__.apply(loader, arguments);
		} else {
			try {
				return __loadScripts__.apply(loader, arguments);
			} catch(ex) {
				loader.lastEx = ex;
				tools.log(tools.LogLevels.Error, "Loader failed :");
				tools.log(tools.LogLevels.Error, ex);
				setTimeout(loader.onerror, 1);
				return false;
			};
		};
	};

	
	doodad.Loader = loader = new (tools.createEventTarget(loader))();

	
	loader.initialized = true;
};


(function(loader) {
	"use strict";

	var __prevOnLoad__ = window.onload;
	window.onload = function loaderWindowOnLoad(ev) {
		if (!loader.initialized) {
			loader.init();
		};
		if (__prevOnLoad__) {
			__prevOnLoad__();
		};
		loader.startup = true;
		loader.loadScripts();
	};
})(Doodad.Loader);


Doodad.scriptsBefore = Doodad.scriptsBefore || [];
Doodad.scriptsAfter = Doodad.scriptsAfter || [];
